import { posix as path } from 'path'
import fs from 'fs'
import { promises as fsp } from 'fs'

import dayjs from 'dayjs'
import Koa from 'koa'
import mount from 'koa-mount'
import koaStatic from 'koa-static'
import fetch from 'node-fetch'
import { fileURLToPath } from 'url'

/** @type{import('./config.json')} */
let config = {}
let repositories = config.repositories

const __dirname = fileURLToPath(new URL('.', import.meta.url))
const dirPublic = path.join(__dirname, './public')
const indexFile = path.join(dirPublic, 'index.html')

/** @type{Set<String>} */
const pomSet = new Set()

const ignoreFiles = [
    'bundle.index.js',
    'bundle.index.js.map',
    'bundle.lib.js',
    'favicon.ico',
    'index.html',
]

async function start() {
    config = JSON.parse(await fsp.readFile(path.join(__dirname, 'config.json')))
    repositories = config.repositories

    const app = new Koa()
    app.use(async (ctx, next) => {
        const ip = ctx.req.socket.remoteAddress
        console.log(datetime(), ip, ctx.method, ctx.originalUrl)
        try {
            await next()
        } catch (err) {
            console.error(err)
            ctx.response.status = err.statusCode || err.status || 500
            ctx.response.body = err.stack
        }
    })

    app.use(mount(config.path, koaStatic(dirPublic, { defer: false })))
    app.use(mount('/', koaStatic(dirPublic, { defer: false, maxage: 10 * 60 * 1000 })))

    app.use(async (ctx) => {

        let pathname = ctx.URL.pathname

        console.log(datetime(), 'pathname', pathname)

        if (pathname == '/log-view') {
            return await responseIndexFile(200, ctx)
        }

        if (pathname == '/list-files') {
            return await responseListFiles(ctx)
        }

        if (pathname == '/log-content') {
            return await responseLogContent(ctx)
        }

        if (pathname == '/search') {
            return await responseSearchResult(ctx)
        }

        if (!pathname.startsWith(config.path)) {
            return ctx.redirect(config.path)
        }

        let subPath = pathname.replace(config.path, '')
        let localFile = path.join(dirPublic, subPath)

        if (fs.existsSync(localFile) && fs.statSync(localFile).isDirectory()) {
            return await responseIndexFile(200, ctx)
        }

        if (pathname.includes('SNAPSHOT')) {
            console.log(datetime(), `ignore SNAPSHOT library : ${pathname}`)
            return await responseIndexFile(404, ctx)
        }

        if (pathname.endsWith('/')) {
            console.log(datetime(), `ignore directory path : ${pathname}`)
            return await responseIndexFile(404, ctx)
        }

        let exists = await checkRemoteExists(ctx)

        if (!exists) {
            return await responseIndexFile(404, ctx)
        }

        await tryCacheFile(ctx)

        if (!fs.existsSync(localFile)) {
            return await responseIndexFile(404, ctx)
        }

        if (fs.statSync(localFile).isDirectory()) {
            return await responseIndexFile(200, ctx)
        }

        console.log(datetime(), 'redirect', pathname)
        return ctx.redirect(pathname)
    })

    await initPomSet()

    app.listen(config.port)
    console.info(`service start at ${config.port}`)
}

function datetime() {
    return dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')
}

/**
 * @param {Koa.ParameterizedContext} ctx 
 */
async function checkRemoteExists(ctx) {
    let pathname = ctx.URL.pathname
    let subPath = pathname.replace(config.path, '')

    let loadingPromise = loadingMap.get('check-remote:' + subPath)
    if (loadingPromise) {
        return loadingPromise
    }

    /** @type{{ count: 0 }} */
    let checkFailedIgnore = loadingMap.get('check-remote-failed-count:' + subPath)
    if (!checkFailedIgnore) {
        checkFailedIgnore = { count: 0 }
        loadingMap.set('check-remote-failed-count:' + subPath, checkFailedIgnore)
    }

    // 忽略错误次数超过100次的lib
    if (checkFailedIgnore.count > 100) {
        console.log(datetime(), `ignore by count ${checkFailedIgnore.count}`, subPath)
        return false
    }

    loadingPromise = new Promise((resolve) => {
        (async () => {
            const sleep = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout))
            let promises = repositories.map(repository => new Promise((resolve) => {
                (async () => {
                    try {
                        let joinUrl = path.join(repository, subPath)
                        // console.log(datetime(),`fetch : ${joinUrl}`)
                        let resp = await fetch(joinUrl, {})
                        if (resp.ok) {
                            console.log(datetime(), `check success : ${joinUrl}`)
                            resolve(true)
                            return
                        } else {
                            checkFailedIgnore.count++
                        }
                    } catch (error) {
                        checkFailedIgnore.count++
                    }
                    // console.log(datetime(),`error : ${repository}`)
                    await sleep(10_000)
                    resolve(false)
                })()
            }))

            let result = false
            try {
                result = await Promise.race(promises)
            } catch (ignore) {
                // 
            }
            loadingMap.delete('check-remote:' + subPath)

            resolve(result)
        })()
    })

    loadingMap.set('check-remote:' + subPath, loadingPromise)
    return loadingPromise
}

/**
 * @param {Koa.ParameterizedContext} ctx 
 */
async function responseListFiles(ctx) {
    /** @type{String} */
    let dir = ctx.query['dir']
    dir = dir.replace(config.path, '')
    let fullDir = path.join(dirPublic, dir)
    let items = []
    if (!fs.existsSync(fullDir)) {
        ctx.response.body = items
        return
    }
    let files = await fsp.readdir(fullDir, { withFileTypes: true })
    for (const item of files) {
        if (ignoreFiles.includes(item.name)) continue
        let fullPath = path.join(fullDir, item.name)
        let url = path.join(config.path, dir, item.name)
        let stat = await fsp.stat(fullPath)
        if (stat.isDirectory()) {
            url += '/'
        }
        items.push({
            file: item.name,
            url: url,
            isdir: stat.isDirectory(),
            length: stat.size,
            mtime: stat.mtime.getTime(),
        })
    }
    ctx.response.body = items
}

/**
 * @param {Koa.ParameterizedContext} ctx 
 */
async function responseLogContent(ctx) {
    /** @type{String} */
    let from = ctx.query['from']
    if (!fs.existsSync(config.logFile)) {
        ctx.response.body = ' Empty! '
        return
    }
    let start = parseInt(from) || 0
    let logFileContent = await fsp.readFile(config.logFile, 'utf-8',)
    ctx.response.body = logFileContent.substr(start)
}

/**
 * @param {Koa.ParameterizedContext} ctx 
 */
async function responseSearchResult(ctx) {
    /** @type{String} */
    let q = ctx.query['q']
    let results = []
    let queries = q.split('')
    for (const item of pomSet) {
        let distance = countCosDistance(queries, item.split(''))
        if (distance > 0) {
            results.push({ dir: config.path + '/' + item, distance })
        }
    }
    ctx.response.body = results.sort((h, l) => l.distance - h.distance).map(o => o.dir)
}

const loadingMap = new Map()

/**
 * @param {Koa.ParameterizedContext} ctx 
 */
async function tryCacheFile(ctx) {
    let pathname = ctx.URL.pathname
    let subPath = pathname.replace(config.path, '')
    let localFile = path.join(dirPublic, subPath)
    console.log(datetime(), 'try cache file >>>>>>> ', pathname)

    let promise = loadingMap.get(subPath)
    if (promise) {
        await promise
        loadingMap.delete(subPath)
        return
    }

    promise = new Promise((resolve) => {
        (async () => {
            await fsp.mkdir(path.dirname(localFile), { recursive: true })
            for (const repository of repositories) {
                try {
                    let joinUrl = repository + subPath
                    console.log(datetime(), 'wait fetch :', joinUrl)
                    let resp = await fetch(joinUrl)
                    if (!resp.ok) throw resp
                    let buffer = await resp.buffer()
                    await fsp.writeFile(localFile, buffer)
                    if (fs.existsSync(localFile)) {
                        initPomSet()
                        console.log(datetime(), 'fetch success :', joinUrl)
                        break
                    }
                } catch (error) {
                    console.warn(error)
                }
            }
            resolve()
        })()
    })
    loadingMap.set(subPath, promise)
    await promise
    loadingMap.delete(subPath)
}

async function initPomSet() {
    pomSet.clear()
    let time = Date.now()
    async function loop(dir) {
        let files = await fsp.readdir(dir, { withFileTypes: true })
        for (const item of files) {
            let fullPath = path.join(dir, item.name)
            if (item.isDirectory()) {
                await loop(fullPath)
                continue
            }
            if (fullPath.endsWith('.pom')) {
                let name = fullPath.replace(dirPublic, '')
                name = name.replace(/\/[^/]+$/, '/')
                pomSet.add(name)
            }
        }
    }
    await loop(dirPublic)
    console.log(datetime(), `initPomSet used time : ${Date.now() - time}ms`)
}

/**
 * 
 * @param {Number} statusCode 
 * @param {Koa.ParameterizedContext} ctx 
 */
async function responseIndexFile(statusCode, ctx) {
    ctx.response.status = statusCode
    ctx.body = await fsp.readFile(indexFile, 'utf-8')
}

/**
 * 计算 a,b 两个数组的余弦距离
 * 
 * @param {object[]} a 
 * @param {object[]} b 
 */
function countCosDistance(a, b) {
    let dict = [...new Set([...a, ...b])]
    let da = []
    let db = []
    for (let i = 0; i < dict.length; i++) {
        const c = dict[i]
        let counta = 0
        let countb = 0
        for (let j = 0; j < a.length; j++) {
            if (c == a[j]) counta++
        }
        for (let j = 0; j < b.length; j++) {
            if (c == b[j]) countb++
        }
        da[i] = counta
        db[i] = countb
    }
    let t = 0
    let ba = 0
    let bb = 0
    for (let i = 0; i < dict.length; i++) {
        const va = da[i]
        const vb = db[i]
        t += va * vb
        ba += va * va
        bb += vb * vb
    }
    return t / (Math.sqrt(ba) * Math.sqrt(bb))
}

start()